"use client"; import { FaRedo } from "react-icons/fa"; import { HUD, HudStat, HudValue, HudLabel, ComboValue, GameArea, UpcomingWrap, UpcomingLabel, UpcomingText, CurrentWrap, LineTimingRow, LineTimingMeta, LineTimingValue, LineTimingBar, LineTimingFill, CharRow, WordWrap, CharBox, ClearToast, GetReadyText, CompletedLineFade, GameFooter, ControlBtn, ProgressWrap, ProgressFill, TimeText, StartOverlay, StartCard, SongTitleText, CountdownNumber, } from "../page.styles"; import { formatTime, calculateCPSNeeded, GameLine } from "../game.utils"; import { GState } from "../game.stat"; type GamePhase = "idle" | "countdown" | "playing" | "paused" | "finished"; interface PlayingViewProps { phase: GamePhase; countdown: number; g: GState; accuracy: number; wpm: number; gameLines: GameLine[]; currentMs: number; duration: number; progressPct: number; lineTimingPct: number; lineRemainingMs: number; currentLineTime: number; intermissionData: { pct: number; remainingMs: number }; wrongChar: boolean; clearShowing: boolean; comboAnimKey: number; wrapSpaceIndicators: boolean[]; charRowRef: React.MutableRefObject; charRefs: React.MutableRefObject<(HTMLSpanElement | null)[]>; onRestart: () => void; } export default function PlayingView({ phase, countdown, g, accuracy, wpm, gameLines, currentMs, duration, progressPct, lineTimingPct, lineRemainingMs, currentLineTime, intermissionData, wrongChar, clearShowing, comboAnimKey, wrapSpaceIndicators, charRowRef, charRefs, onRestart, }: PlayingViewProps) { return ( <> {phase === "countdown" && ( Get Ready {countdown} )} {g.score.toLocaleString()} Score 0} key={`combo-${comboAnimKey}`}> x{g.combo} Combo {accuracy}% Accuracy {wpm} WPM {g.totalMiss} Misses {phase === "playing" && g.displayedLineIdx < 0 && gameLines.length > 0 && ( <> Next {gameLines[0] && gameLines[0].content.trim() === "" ? "[INTERMISSION]" : (gameLines[0]?.content ?? "")} Time to first line:{" "} {Math.max(0, intermissionData.remainingMs / 1000).toFixed(1)}s
{intermissionData.remainingMs > 5000 && "Press Space to skip long intermissions"}
[INTERMISSION]
)} {g.displayedLineIdx >= 0 && gameLines[g.displayedLineIdx] && ( <> Next {gameLines[g.displayedLineIdx + 1] && gameLines[g.displayedLineIdx + 1].content.trim() === "" ? "[INTERMISSION]" : (gameLines[g.displayedLineIdx + 1]?.content ?? "")} Time left:{" "} {Math.max(0, lineRemainingMs / 1000).toFixed(1)}s {gameLines[g.displayedLineIdx].content.trim() !== "" && ( Estimated CPS:{" "} {calculateCPSNeeded(gameLines[g.displayedLineIdx].content, currentLineTime / 1000).toFixed(1)} )} {gameLines[g.displayedLineIdx].content.trim() !== "" && (() => { const rawText = gameLines[g.displayedLineIdx].content; const text = rawText.toLowerCase(); const tokens = text.split(/(\s+)/).filter(Boolean); let renderIndex = 0; return tokens.flatMap((token, tokenIdx) => { if (/^\s+$/.test(token)) { return token.split("").map((ch, spaceIdx) => { let state: "typed" | "active" | "pending" | "wrong"; if (renderIndex < g.typedCount) state = "typed"; else if (renderIndex === g.typedCount) state = wrongChar ? "wrong" : "active"; else state = "pending"; const charIndex = renderIndex; const showIndicator = ch === " " && wrapSpaceIndicators[charIndex] && state !== "typed"; const displayChar = ch === " " ? (showIndicator ? "␣" : "\u00A0") : ch; const element = ( { charRefs.current[charIndex] = el; }} > {displayChar} ); renderIndex += 1; return element; }); } const wordChars = token.split("").map((ch, charIdx) => { let state: "typed" | "active" | "pending" | "wrong"; if (renderIndex < g.typedCount) state = "typed"; else if (renderIndex === g.typedCount) state = wrongChar ? "wrong" : "active"; else state = "pending"; const charIndex = renderIndex; const element = ( { charRefs.current[charIndex] = el; }} > {ch} ); renderIndex += 1; return element; }); return {wordChars}; }); })()} {clearShowing && CLEAR!} {gameLines[g.displayedLineIdx].content.trim() === "" ? "[INTERMISSION]" : g.lineCompleted ? "Cleared - waiting for next line..." : gameLines[g.displayedLineIdx].content} )} {phase === "idle" && ( Start the game to begin typing )}
{formatTime(Math.max(0, currentMs))} / {formatTime(duration)} ); }